りおんクロニクル


SQLite × EF Core 高度編|リレーション・ナビゲーション完全ガイド【2026年版】

Home【2026年版】C# / .NET入門と実践ガイド|基礎・業務アプリ開発・SQLite連携まで体系的に解説

SQLiteはシンプルなRDBですが、EF Coreと組み合わせるとリレーション設計が一気に強力になります。 一方で、外部キー・Cascade削除・多対多などを正しく理解していないと、 「意図しない削除」「N+1問題」「パフォーマンス劣化」が起きがちです。 この記事では、SQLite × EF Core のリレーション設計と実務パターンを整理します。

この記事でわかること
・1対多・多対多のモデル定義
・外部キーとナビゲーションプロパティの関係
・Include / ThenInclude の使いどころ
・Cascade / Restrict など削除動作の制御
・SQLite特有の制限と注意点
・業務アプリ向けベストプラクティス

1. 前提:サンプルドメイン(顧客と注文)

この記事では、よくある「顧客(Customer)と注文(Order)」を例にします。

■ モデル定義(1対多)

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }

    // 1対多:Customer 1人に複数の Order
    public ICollection<Order> Orders { get; set; } = new List<Order>();
}

public class Order
{
    public int Id { get; set; }
    public DateTime OrderedAt { get; set; }
    public decimal Amount { get; set; }

    // 外部キー
    public int CustomerId { get; set; }

    // ナビゲーション(多対1)
    public Customer Customer { get; set; }
}

2. DbContextでのリレーション構成

EF Coreは命名規約だけでもリレーションを推論しますが、 業務アプリでは OnModelCreating で明示的に定義しておく方が安全です。

■ DbContext定義

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Order> Orders { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=app.db");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Customer - Order : 1対多
        modelBuilder.Entity<Customer>()
            .HasMany(c => c.Orders)
            .WithOne(o => o.Customer)
            .HasForeignKey(o => o.CustomerId)
            .OnDelete(DeleteBehavior.Restrict); // or Cascade
    }
}

■ OnDelete(DeleteBehavior.XXX) の意味

注意: 業務アプリでは、意図せぬ一括削除を防ぐために Restrict を選ぶケースが多いです。

3. Include / ThenInclude で関連データを一括取得

ナビゲーションプロパティは、デフォルトでは遅延読み込みされません(SQLite+EF Core標準ではオフ)。 関連データを一度に取得したい場合は Include / ThenInclude を使います。

■ 顧客とその注文一覧を一括取得

using var db = new AppDbContext();

var customers = await db.Customers
    .Include(c => c.Orders)
    .ToListAsync();

foreach (var c in customers)
{
    Console.WriteLine($"{c.Name} の注文数: {c.Orders.Count}");
}

■ ネストしたリレーション(ThenInclude)

例えば、Order に OrderLines(明細)がある場合:

var customers = await db.Customers
    .Include(c => c.Orders)
        .ThenInclude(o => o.OrderLines)
    .ToListAsync();

■ N+1問題を防ぐ

Include を使わずにループ内でナビゲーションを辿ると、 毎回クエリが発行される(N+1問題)ので注意。

4. 多対多(Many-to-Many)の定義

EF Core 5以降では、中間テーブルを明示的に書かなくても多対多を定義できます。 例:User と Role の多対多。

■ モデル定義(簡易多対多)

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<Role> Roles { get; set; } = new List<Role>();
}

public class Role
{
    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<User> Users { get; set; } = new List<User>();
}

■ DbContext(暗黙の中間テーブル)

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>()
        .HasMany(u => u.Roles)
        .WithMany(r => r.Users)
        .UsingEntity(j => j.ToTable("UserRoles"));
}

これで、UserRoles という中間テーブルが自動生成されます。

■ 多対多の操作

var user = await db.Users
    .Include(u => u.Roles)
    .FirstAsync(u => u.Id == 1);

var adminRole = await db.Roles.FirstAsync(r => r.Name == "Admin");

user.Roles.Add(adminRole);
await db.SaveChangesAsync();

5. SQLite特有の制限と注意点

■ 外部キー制約は明示的に有効化が必要な場合がある

SQLiteは PRAGMA foreign_keys = ON; が必要なケースがあります。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlite("Data Source=app.db");
}

public override int SaveChanges()
{
    Database.ExecuteSqlRaw("PRAGMA foreign_keys = ON;");
    return base.SaveChanges();
}

(最近のEF Core+UseSqliteでは自動ONになるケースもありますが、明示しておくと安心)

■ Cascade削除の挙動

6. 削除動作(Cascade / Restrict)の設計指針

業務アプリでは、削除ポリシーを明確にしておくことが重要です。

■ よくあるパターン

■ 論理削除の例

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsDeleted { get; set; }
}

クエリ側で Where(c => !c.IsDeleted) を標準にする。

7. 実務で使えるクエリパターン集

■ 顧客と直近の注文1件を取得

var customers = await db.Customers
    .Select(c => new
    {
        Customer = c,
        LastOrder = c.Orders
            .OrderByDescending(o => o.OrderedAt)
            .FirstOrDefault()
    })
    .ToListAsync();

■ 特定期間に注文がある顧客だけ取得

var from = new DateTime(2026, 1, 1);
var to   = new DateTime(2026, 12, 31);

var customers = await db.Customers
    .Where(c => c.Orders.Any(o => o.OrderedAt >= from && o.OrderedAt <= to))
    .ToListAsync();

■ 顧客ごとの注文合計金額

var result = await db.Customers
    .Select(c => new
    {
        c.Id,
        c.Name,
        TotalAmount = c.Orders.Sum(o => (decimal?)o.Amount) ?? 0m
    })
    .ToListAsync();

8. 業務アプリ向けベストプラクティス

まとめ:SQLite × EF Core リレーションは“設計がすべて”

「とりあえず動く」EF Coreから、 「壊れず・読みやすく・保守しやすい」リレーション設計へ。 この記事のパターンをベースに、実際のドメインに合わせてモデルを組み立ててみてください。

前のページ  次のページ